package com.acme.hashmap;

import com.acme.hash.HashConvolution;
import com.acme.hash.SmearIntHashFunctionImpl;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

/**
 * @author Gregory Sobko
 */
public class ByteBufferLinkedHashMapIntToInt implements MapIntToInt {

    public final int NULL_POINTER = -1;

    private final int hashCapacity;

    private final int linkedStorageCapacity;

    private final int hashOffset;

    private boolean storageFull = false;

    private int linkedStoragePointer = -1;

    private int size = 0;

    private int FIRST_REMOVED_INDEX = NULL_POINTER;

    private HashConvolution hashFunction;

    private IntBuffer storage;

    public ByteBufferLinkedHashMapIntToInt(ByteBuffer storage) {
        this.storage = storage.asIntBuffer();
        int totalCapacity = this.storage.capacity();
        int capacity = (totalCapacity - (totalCapacity % 4)) / 4;
        if (capacity == 0) {
            throw new NotEnoughCapacityException("Not enough capacity for storing even one element! " +
                    "ByteBuffer size shouldn't be less than 16");
        }
        hashOffset = 3 * capacity;
        linkedStorageCapacity = hashCapacity = capacity;
        hashFunction = new HashConvolution(new SmearIntHashFunctionImpl());
        hashFunction.setDimension(hashCapacity);
        initHashTable();
        initLinkedStorage();
    }

    /**
     * {@inheritDoc}
     * @throws NotEnoughCapacityException if storage is full
     */
    @Override
    public Integer put(int key, int value) {
        int hash = this.hashFunction.hash(key);
        int firstIndex = getAddressForStoredHash(hash);
        int previous;
        if (firstIndex == NULL_POINTER) {
            incrementSize();
            setAddressForStoredHash(hash, putElement(linkedStoragePointer, key, value));
        } else {
            int currentIndex = firstIndex;
            int nextIndex = currentIndex;
            do {
                if (key == getElementKey(nextIndex)) {
                    previous = getElementValue(nextIndex);
                    setElementValue(nextIndex, value);
                    return previous;
                }
                currentIndex = nextIndex;
            } while ((nextIndex = getElementNext(nextIndex)) != NULL_POINTER);
            incrementSize();
            setElementNext(currentIndex, putElement(linkedStoragePointer, key, value));
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Integer get(int key) {
        int hash = this.hashFunction.hash(key);
        return getValueByKey(getAddressForStoredHash(hash), key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains(int key) {
        return get(key) != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Integer remove(int key) {
        Integer value = get(key);
        if (value != null) {
            int hash = hashFunction.hash(key);
            int firstIndex = getAddressForStoredHash(hash);
            setAddressForStoredHash(hash, chainFindAndRemove(key, firstIndex));
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clear() {
        initHashTable();
        initLinkedStorage();
        size = 0;
        linkedStoragePointer = 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int size() {
        return size;
    }

    // Used in remove method
    private int chainFindAndRemove(int keyLookup, int currentNext) {
        int result = currentNext;
        if (currentNext != NULL_POINTER) {
            if (getElementKey(currentNext) == keyLookup) {
                result = getElementNext(currentNext);
                pushRemoved(currentNext);
                size--;
            } else {
                setElementNext(currentNext, chainFindAndRemove(keyLookup, getElementNext(currentNext)));
            }
        }
        return result;
    }

    // Used in get method
    private Integer getValueByKey(int index, int key) {
        if (index == NULL_POINTER) {
            return null;
        } else if (getElementKey(index) == key) {
            return getElementValue(index);
        } else {
            return getValueByKey(getElementNext(index), key);
        }
    }

    // Increments size and moves linkedStoragePointer to next position
    private void incrementSize() {
        linkedStoragePointer = nextLinkedStoragePointer();
    }

    // returns next linkedStoragePointer
    private int nextLinkedStoragePointer() {
        int nextStoragePointer = linkedStoragePointer;
        if (storageFull) {
            nextStoragePointer = popRemoved();
        } else {
            nextStoragePointer++;
            if (nextStoragePointer >= linkedStorageCapacity) {
                storageFull = true;
                nextStoragePointer = popRemoved();
            }
        }
        if (nextStoragePointer == NULL_POINTER) {
            throw new NotEnoughCapacityException("Not enough capacity");
        } else {
            size++;
        }
        return nextStoragePointer;

    }

    // Pops removed element to removed element stack
    private int popRemoved() {
        int result = FIRST_REMOVED_INDEX;
        if (result == NULL_POINTER) {
            return result;
        }
        FIRST_REMOVED_INDEX = getElementNext(FIRST_REMOVED_INDEX);
        return result;
    }

    // Pushes removed element to removed element stack
    private void pushRemoved(int storageIndex) {
        setElementNext(storageIndex, FIRST_REMOVED_INDEX);
        FIRST_REMOVED_INDEX = storageIndex;

    }

    //-------- ELEMENT STORAGE HELPERS ----------
    private int getElementKey(int storageIndex) {
        return storage.get(3 * storageIndex);
    }

    private int getElementValue(int storageIndex) {
        return storage.get(3 * storageIndex + 1);
    }

    private int getElementNext(int storageIndex) {
        return storage.get(3 * storageIndex + 2);
    }

    private void setElementKey(int storageIndex, int key) {
        storage.put(3 * storageIndex, key);
    }

    private void setElementValue(int storageIndex, int value) {
        storage.put(3 * storageIndex + 1, value);
    }

    private void setElementNext(int storageIndex, int next) {
        storage.put(3 * storageIndex + 2, next);
    }

    private int putElement(int storageIndex, int key, int value) {
        setElementKey(storageIndex, key);
        setElementValue(storageIndex, value);
        setElementNext(storageIndex, NULL_POINTER);
        return storageIndex;
    }
    //-------- END ELEMENT STORAGE HELPERS ----------


    //-------- HASHTABLE STORAGE HELPERS ----------
    private void setAddressForStoredHash(int index, int address) {
        storage.put(hashOffset + index, address);

    }
    private int getAddressForStoredHash(int index) {
        return storage.get(hashOffset + index);
    }


    //-------- END HASHTABLE STORAGE HELPERS ----------


    //-------- INITIALIZERS ----------
    private void initHashTable() {
        for (int i = 0; i < hashCapacity; i++) {
            setAddressForStoredHash(i, NULL_POINTER);
        }
    }

    private void initLinkedStorage() {
        clearLinkedStorageLinks();
    }

    private void clearLinkedStorageLinks() {
        for (int i = 0; i < linkedStorageCapacity; i++) {
            setElementNext(i, NULL_POINTER);
        }
    }
    //-------- END INITIALIZERS ----------

    // big fat toString method, visualizing bucket distribution, just for illustrative needs
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("ByteBufferLinkedHashMapIntToInt{ linkedStoragePointer=");
        stringBuilder.append(linkedStoragePointer);
        stringBuilder.append(", size=");
        stringBuilder.append(size);
        stringBuilder.append(", FIRST_REMOVED_INDEX=");
        stringBuilder.append(FIRST_REMOVED_INDEX);
        stringBuilder.append("}\n====STRORAGE INTERNALS====\n");
        stringBuilder.append("[");
        for (int i = 0; i < storage.capacity(); i++) {
            stringBuilder.append(storage.get(i));
            if (i != storage.capacity() - 1) {
                stringBuilder.append(", ");
            }
        }
        stringBuilder.append("]\n");
        for (int i = 0; i < hashCapacity; i++) {
            stringBuilder.append(i);
            stringBuilder.append(" -> [");
            int next = getAddressForStoredHash(i);
            while (next != NULL_POINTER) {
                stringBuilder.append(" {");
                stringBuilder.append(getElementKey(next));
                stringBuilder.append(":");
                stringBuilder.append(getElementValue(next));
                stringBuilder.append("} ");
                next = getElementNext(next);
            }
            stringBuilder.append("]\n");
        }
        int next = FIRST_REMOVED_INDEX;
        stringBuilder.append("removed stack -> [");
        while (next != NULL_POINTER) {
            stringBuilder.append(" {");
            stringBuilder.append(getElementKey(next));
            stringBuilder.append(":");
            stringBuilder.append(getElementValue(next));
            stringBuilder.append("} ");
            next = getElementNext(next);
        }
        stringBuilder.append("]\n====STRORAGE INTERNALS END====\n");
        return stringBuilder.toString();
    }
}
